package com.open.common.utils;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.fastjson2.JSON;
import com.open.common.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Excel工具类 - 基于EasyExcel实现
 * 统一使用byte[]方式导出（类似genCode方法）
 *
 * @author 赵浩埼
 * @date 2025-10-29
 */
@Slf4j
public class ExcelUtils {
    
    /**
     * 读取Excel文件，返回数据列表（带表头校验）
     *
     * @param file  上传的Excel文件
     * @param clazz 读取的数据类型
     * @return 数据列表
     */
    public static <T> List<T> readExcelSync(MultipartFile file, Class<T> clazz) {
        try {
            // 获取期望的表头
            List<String> expectedHeaders = getExcelHeaders(clazz);
            
            // 创建监听器来校验表头
            HeaderValidationListener<T> listener = new HeaderValidationListener<>(expectedHeaders);
            
            InputStream inputStream = file.getInputStream();
            
            // 读取Excel并校验表头
            EasyExcel.read(inputStream, clazz, listener)
                    .sheet()
                    .doRead();
            
            log.info("Excel同步读取成功，文件名：{}，数据行数：{}", file.getOriginalFilename(), listener.getDataList().size());
            return listener.getDataList();
        } catch (ServiceException e) {
            throw e;
        } catch (IOException e) {
            log.error("Excel读取失败", e);
            throw new ServiceException("Excel读取失败：" + e.getMessage());
        } catch (Exception e) {
            log.error("Excel解析失败", e);
            throw new ServiceException("Excel解析失败，请按照模板格式导入");
        }
    }
    
    /**
     * 从类的@ExcelProperty注解中获取表头信息
     *
     * @param clazz Excel对应的类
     * @return 表头列表
     */
    private static <T> List<String> getExcelHeaders(Class<T> clazz) {
        Map<Integer, String> headerMap = new TreeMap<>();
        
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
            if (excelProperty != null) {
                String[] values = excelProperty.value();
                int index = excelProperty.index();
                if (values.length > 0) {
                    headerMap.put(index, values[0]);
                }
            }
        }
        
        return new ArrayList<>(headerMap.values());
    }
    
    /**
     * 表头校验监听器
     */
    private static class HeaderValidationListener<T> extends AnalysisEventListener<T> {
        private final List<String> expectedHeaders;
        private final List<T> dataList = new ArrayList<>();
        private boolean headerValidated = false;
        
        public HeaderValidationListener(List<String> expectedHeaders) {
            this.expectedHeaders = expectedHeaders;
        }
        
        @Override
        public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
            // 获取实际的表头
            List<String> actualHeaders = headMap.entrySet().stream()
                    .sorted(Map.Entry.comparingByKey())
                    .map(Map.Entry::getValue)
                    .collect(Collectors.toList());
            
            // 校验表头是否一致
            if (expectedHeaders.size() != actualHeaders.size()) {
                log.error("Excel表头数量不匹配，期望：{}，实际：{}", expectedHeaders.size(), actualHeaders.size());
                throw new ServiceException("Excel格式不正确，请按照模板格式导入（表头数量不匹配）");
            }
            
            for (int i = 0; i < expectedHeaders.size(); i++) {
                String expected = expectedHeaders.get(i);
                String actual = actualHeaders.get(i);
                if (!expected.equals(actual)) {
                    log.error("Excel表头不匹配，位置{}，期望：{}，实际：{}", i, expected, actual);
                    throw new ServiceException(
                            String.format("Excel格式不正确，请按照模板格式导入（第%d列应为\"%s\"，实际为\"%s\"）", 
                                    i + 1, expected, actual));
                }
            }
            
            headerValidated = true;
            log.info("Excel表头校验通过");
        }
        
        @Override
        public void invoke(T data, AnalysisContext context) {
            if (!headerValidated) {
                throw new ServiceException("Excel表头校验失败");
            }
            dataList.add(data);
        }
        
        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            log.info("Excel数据解析完成，共{}条数据", dataList.size());
        }
        
        public List<T> getDataList() {
            return dataList;
        }
    }

    /**
     * 导出Excel为byte数组（单sheet）
     *
     * @param data      导出的数据列表
     * @param clazz     导出数据类型
     * @return byte数组
     */
    public static <T> byte[] exportExcelToBytes(List<T> data, Class<T> clazz) {
        return exportExcelToBytes(data, clazz, null);
    }

    /**
     * 导出Excel为byte数组（单sheet）
     *
     * @param data      导出的数据列表
     * @param clazz     导出数据类型
     * @param sheetName 工作表名称（默认：Sheet1）
     * @return byte数组
     */
    public static <T> byte[] exportExcelToBytes(List<T> data, Class<T> clazz, String sheetName) {
        log.info("Excel单sheet导出，data：{}, clazz：{}, sheetName：{}", JSON.toJSONString(data), clazz, sheetName);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            if (sheetName == null || sheetName.trim().isEmpty()) {
                sheetName = "Sheet1";
            }
            EasyExcel.write(outputStream, clazz)
                    .sheet(sheetName)
                    .doWrite(data);
            log.info("Excel导出为字节数组成功，sheet名称：{}", sheetName);
            return outputStream.toByteArray();
        } catch (Exception e) {
            log.error("Excel导出为字节数组失败", e);
            throw new ServiceException("Excel导出为字节数组失败：" + e.getMessage());
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                log.error("关闭输出流失败", e);
            }
        }
    }

    /**
     * 导出Excel为byte数组（多sheet）
     *
     * @param dataMap key为sheet名称，value为该sheet的数据列表
     * @param clazz   导出数据类型
     * @return byte数组
     */
    public static <T> byte[] exportMultiSheetToBytes(Map<String, List<T>> dataMap, Class<T> clazz) {
        log.info("Excel多sheet导出，dataMap：{}, clazz：{}", JSON.toJSONString(dataMap), clazz);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ExcelWriter excelWriter = null;
        try {
            excelWriter = EasyExcel.write(outputStream, clazz).build();
            int sheetNo = 0;
            for (Map.Entry<String, List<T>> entry : dataMap.entrySet()) {
                String sheetName = entry.getKey();
                List<T> data = entry.getValue();
                WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo++, sheetName).build();
                excelWriter.write(data, writeSheet);
            }
            return outputStream.toByteArray();
        } catch (Exception e) {
            log.error("Excel多sheet导出为字节数组失败", e);
            throw new ServiceException("Excel多sheet导出为字节数组失败：" + e.getMessage());
        } finally {
            if (excelWriter != null) {
                excelWriter.finish();
            }
            try {
                outputStream.close();
            } catch (IOException e) {
                log.error("关闭输出流失败", e);
            }
        }
    }

    /**
     * 通过byte数组直接输出文件
     *
     * @param response HttpServletResponse
     * @param data    Excel文件的字节数组
     * @param fileName 文件名（包含扩展名，如：export.xlsx）
     */
    public static void genCode(HttpServletResponse response, byte[] data, String fileName) {
        response.reset();

        try {
            // 对文件名进行URL编码，解决中文乱码问题
            String encodedFileName = URLEncoder.encode(fileName, "UTF-8")
                    .replaceAll("\\+", "%20"); // 替换空格

            response.setHeader("Content-Disposition",
                    "attachment; filename=" + encodedFileName);

        response.addHeader("Content-Length", "" + data.length);
        response.setContentType("application/octet-stream; charset=UTF-8");
        response.setCharacterEncoding("UTF-8");

        OutputStream outputStream = response.getOutputStream();
            outputStream.write(data);
            outputStream.flush();
        } catch (Exception e) {
            log.error("生成{}文件失败", fileName, e);
        }
    }
}

